feat(ai): multi-model ensemble engine with dynamic weighting (#10)#170
feat(ai): multi-model ensemble engine with dynamic weighting (#10)#170saidai-bhuvanesh wants to merge 12 commits into
Conversation
|
Someone is attempting to deploy a commit to the karan3431's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR adds a multi-model ensemble scoring system with confidence margins, species/fraud detection to backend scan endpoints, vendor review submission, an offline-first scan queue with background sync, an analytics trends dashboard, and a two-scan comparison feature, plus supporting localization strings across three languages. ChangesMulti-Model Ensemble, Species & Fraud Detection
Vendor Reviews & Leaderboard
Offline Scan Sync
Analytics Trends
Scan Comparison
Estimated code review effort: 4 (Complex) | ~75 minutes Sequence Diagram(s)sequenceDiagram
participant ScannerPage
participant OfflineDb
participant SubmitScanAPI
ScannerPage->>ScannerPage: generate offlineScanRecord
alt online
ScannerPage->>SubmitScanAPI: submitScan(record)
SubmitScanAPI-->>ScannerPage: success/failure
ScannerPage->>OfflineDb: addScan if failed
else offline
ScannerPage->>OfflineDb: addScan(record, pending)
end
ScannerPage->>OfflineDb: getPendingScans (on mount/online)
OfflineDb-->>ScannerPage: pending scans
ScannerPage->>SubmitScanAPI: submitScan(each pending scan)
SubmitScanAPI-->>ScannerPage: result
ScannerPage->>OfflineDb: deleteScan or updateScanStatus(failed)
sequenceDiagram
participant User
participant ResultsPage
participant ScanAPI
User->>ResultsPage: enable compare mode
User->>ResultsPage: select scan A, scan B
ResultsPage->>ScanAPI: getScan(idA), getScan(idB)
ScanAPI-->>ResultsPage: scan A, scan B data
ResultsPage->>ResultsPage: compute variance/decay
ResultsPage-->>User: render comparison modal
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
|
There was a problem hiding this comment.
Actionable comments posted: 36
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/pages/ScannerPage.tsx (1)
397-397: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
runScanclosure omitstfrom its dependency array.The callback now calls
t('scanner.savedOffline', ...)(Line 375) but the deps[startProgress, stopProgress, stopCamera, navigate]don't includet, so a language change could leave a stale translator captured. Addt(and confirmexhaustive-depspasses for the added state setters).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/pages/ScannerPage.tsx` at line 397, The runScan callback in ScannerPage.tsx is missing t in its dependency array, so the translator captured by useCallback can become stale after a language change. Update the dependency list for runScan to include t, and verify any related hooks in ScannerPage still satisfy exhaustive-deps after the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/main.py`:
- Line 393: The uncertain_flag expression in the row processing logic is using a
falsy fallback, so a real confidence_score of 0 or 0.0 gets replaced with 1.0
and marked certain. Update the confidence handling in the code path that builds
the row dict in main.py to use an explicit None check instead of “or 1.0”, so
only missing confidence_score values default while valid zero values are
preserved.
- Around line 257-272: The ensemble weight calculation in the main scan payload
is using random.uniform values, so replace the randomized quality signals and
derived weights with real scan-driven metrics or a fixed 50/25/25 weighting in
the payload-building logic. Update the weight computation in the block that sets
gill_quality, eye_quality, body_quality, weight_body, weight_eye, and
weight_gill, and make the matching _row_to_payload path use the same
deterministic source so ensemble.weights reflects the actual documented
weighting instead of per-request randomness.
- Around line 663-702: The edge ONNX scan path is storing the human-readable
freshness label directly into the scan grade field, so history/dashboard records
the wrong grade. In the edge_onnx branch inside the scan insert flow, map
freshness_label to the same stored grade enum used by the demo/real paths before
calling _db().table("scans").insert, and ensure the value passed through
_to_db_grade matches the app’s existing grade format rather than the raw
"Fresh"/"Moderate"/"Spoiled" label.
In `@backend/vendors.py`:
- Around line 104-166: The default review seed is duplicated in both the GET
fallback and the POST initialization inside the vendors review handlers, which
risks them drifting apart. Extract the repeated seed list into a shared helper
such as _default_reviews() in the same module and use it from both the reviews
lookup and add_vendor_review paths so the seed data is defined in one place
only.
- Around line 139-145: The review creation logic in the vendors endpoint
currently reads `review_data` as a raw dict and casts `rating` with
`int(rating)`, which can raise on bad input and accepts invalid scores. Update
the review input handling around the `new_review` construction to use a Pydantic
model (for example, a `ReviewIn` type) with `rating` constrained to 1–5 and a
default `comment`, then access `review_data.rating` and `review_data.comment`
instead of dict lookups and manual casting.
- Around line 8-9: The module-level _REVIEWS_DB fallback in the vendors module
is only in-memory and will not survive restarts or work across multiple workers.
Replace this temporary storage with the same persistent backend used by vendors
and scans, or if it must remain as a stub, add an explicit TODO with an issue
reference so it is clearly temporary. Update the code around _REVIEWS_DB and any
review accessors to use the shared persistent store instead of a dict.
- Around line 129-137: The user lookup in the vendor author resolution block is
passing the Request object into get_current_user, which causes authentication to
fail and fall back to Anonymous Consumer. Update the call in the code path
around auth_header handling to pass the raw Authorization header value instead
of request, and make sure the exception handling in this block does not silently
swallow auth errors so failures are visible. Use the get_current_user call site
and the author assignment logic as the main points to update.
- Around line 122-127: The add_vendor_review route is effectively
unauthenticated because get_current_user is being called with a Request instead
of an Authorization header, so all reviews fall back to Anonymous Consumer.
Update add_vendor_review to require auth via Depends(get_current_user) (or
otherwise pass the proper Authorization value) and use the authenticated user
for review attribution.
In `@src/components/AnalyticsTrends.tsx`:
- Line 4: The AnalyticsTrends component has an unused Calendar import, which is
triggering the no-unused-vars lint error. Update the import in AnalyticsTrends
to remove Calendar, or if it is intended for future use, reference it somewhere
in the component so the import is actually used alongside TrendingUp,
TrendingDown, MapPin, and Award.
- Around line 283-297: The list rendering in AnalyticsTrends uses array indexes
as keys for vendors (and the similar regions list), which can break React
reconciliation when the sorted order changes. Update the map callbacks to use a
stable unique identifier from each item in the AnalyticsTrends render logic,
such as v.name for vendors and the corresponding region identifier for regions,
so re-sorts do not misassociate DOM/state.
- Line 26: The date labels in AnalyticsTrends are hardcoded to the en-IN locale,
so they won’t follow the active language. Update the date formatting logic in
AnalyticsTrends (the dateStr and weekStr computations) to use the current i18n
language from useTranslation() instead of a fixed locale. Keep the formatting
logic centralized so both day and week labels localize consistently for English,
Hindi, and Bengali users.
- Around line 153-169: The tab-toggle buttons in AnalyticsTrends are missing an
explicit type, so they default to submit behavior inside a form. Update both
button elements in the tab switcher to use the button type explicitly while
keeping their existing onClick handlers and styling, so clicking the
daily/weekly tabs only changes activeTab and does not submit a parent form.
- Around line 56-68: The `useMemo` in `AnalyticsTrends` is using `Date.now()`
while building the `vendorMap`, which makes the “recent” cutoff depend on render
time without being tracked as a dependency. Move the current-time value out of
the memoized calculation (or pass it in as an explicit dependency/state) and use
that stable reference inside the `scans.forEach` loop so the split logic
recalculates when time changes, not only when `scans` or `t` changes.
- Around line 82-90: The regional averages data in AnalyticsTrends is hardcoded
and does not use the scans input, so replace the static regions literal with
logic that derives regional values from the scans prop like the daily, weekly,
and vendors sections do. Update the data-building code in AnalyticsTrends to
aggregate scans by region name/key, compute each region’s value and scan count
from actual scan history, and keep the existing t(...) labels only for display
names. Ensure the returned regions array reflects live scan data rather than
fixed mock values.
In `@src/fusionInference.js`:
- Around line 259-266: The confidence calculation in calculateConfidence is
indexing eyeProbs and gillProbs as if they had more than two elements, but
extractEyeScore and extractGillScore only return 2-element vectors. Update
calculateConfidence to use the actual two probabilities for each vector,
recompute the eye and gill confidences from those two values, and ensure the
final system confidence stays normalized so uncertain_flag can trigger
correctly.
In `@src/i18n/locales/bn.json`:
- Around line 262-269: The Bengali leaderboard locale currently has duplicate
aliases for the same strings: the `Leaderboard` page still references
`leaderboard.vendorTrustLeaderboard` and `leaderboard.vendorSubtitle`, so the
added `title` and `subtitle` entries in `src/i18n/locales/bn.json` are unused.
Either update `src/pages/Leaderboard.tsx` to consume the new `title`/`subtitle`
keys consistently, or remove the redundant aliases from the locale and keep the
existing `vendorTrustLeaderboard`/`vendorSubtitle` keys to avoid duplication.
In `@src/i18n/locales/en.json`:
- Line 170: The preservDefault locale string has a capitalization issue after
the period, making the second sentence start incorrectly. Update the text in
en.json for preservDefault so the sentence after “Store below 4°C.” begins with
a capital letter, keeping the rest of the wording unchanged.
In `@src/lib/offlineDb.ts`:
- Around line 23-106: The IndexedDB helpers in offlineDb are leaking open
connections because openDB() is used in addScan, getPendingScans,
updateScanStatus, and deleteScan without ever closing the returned db. Update
each method to close the database after the transaction/request finishes, using
the same transaction.oncomplete/db.close pattern already implied for the first
operation, and make sure the connection is always closed even when the request
fails.
In `@src/pages/AnalysisDashboard.tsx`:
- Line 79: The AnalysisDashboard image preview is creating a blob URL with
URL.createObjectURL(found.image) and never releasing it, which leaks memory.
Update the AnalysisDashboard component to track the generated photo_url value
and add a useEffect cleanup that calls URL.revokeObjectURL for that URL when the
component unmounts or the URL changes, so the created object URL is properly
released.
- Around line 447-491: The interactive spot elements in AnalysisDashboard are
pointer-only because the eye/gill/body hotspots use clickable divs without
button semantics. Update the spot wrappers to use accessible activation in the
same spot-rendering block: preferably switch them to button elements with
type="button", or add role="button", tabIndex={0}, and an onKeyDown handler
alongside the existing onClick/setActiveSpot logic. Ensure the same behavior is
available for each hotspot so keyboard and screen-reader users can select the
active spot and reach the details panel.
- Line 141: The AnalysisDashboard render path assigns alerts from
recommendations.alert_flags but never uses it; remove the unused alerts variable
from the component and keep the surrounding recommendation rendering logic in
place.
In `@src/pages/Leaderboard.tsx`:
- Line 53: The reviews state in Leaderboard is using any[], which triggers the
no-explicit-any lint rule. Define a Review interface with id, author, rating,
comment, and timestamp, then update the useState declaration in Leaderboard to
use Review[] instead of any[] and make sure any review-related code in that
component uses the new type.
- Line 276: The Leaderboard panel still contains hardcoded English literals that
bypass i18n, while nearby UI already uses t() for translated strings. Update the
strings in the Leaderboard component (including the Rating label, the
submit/loading button states, and the empty-state message) to use t() with the
existing locale keys pattern, so all text in this panel is consistently
translated across en/hi/bn.
- Around line 236-241: The close button in Leaderboard.tsx currently relies on
the default submit behavior, and the textarea only has placeholder text without
an accessible name. Update the button in the selected-vendor controls to use
type="button" so it won’t submit a parent form, and add an aria-label to the
textarea in the feedback/input section so screen readers can identify it. Use
the existing button and textarea elements in Leaderboard to locate the fix.
- Line 254: The selected vendor freshness score rendering in Leaderboard.tsx can
crash when avg_freshness_score is null or undefined. Update the selectedVendor
display to use the same null-safe fallback as the list view, in the
selectedVendor rendering path, so it formats a default numeric value before
calling toFixed(1). Keep the fix localized to the
selectedVendor.avg_freshness_score usage.
- Around line 74-80: The authorization header in Leaderboard is reading the
wrong storage key, so the Bearer token is usually missing. Update the token
lookup in Leaderboard to use the shared token helper from src/lib/api.ts instead
of reading 'supabase.auth.token' directly, and make the request reuse that
centralized auth logic so the Authorization header is attached consistently.
- Around line 59-67: The reviews loading effect in Leaderboard is vulnerable to
stale responses and ignores HTTP failures. Update the useEffect around
selectedVendor/fetch to check response status before parsing, and add request
cancellation or an ignore flag so earlier requests cannot overwrite newer vendor
reviews. Use the existing setReviewsLoading flow and the selectedVendor
dependency to ensure only the latest vendor’s data updates state.
In `@src/pages/ResultsPage.tsx`:
- Around line 119-138: Apply the appropriate PR complexity label for this
compare-mode change in ResultsPage; the new compare flow and selection state
around compareMode and setSelectedIds fits complexity: intermediate, so add that
label unless you are labeling the broader stacked ensemble/ML pipeline work, in
which case use complexity: high. Follow the repo’s auto-labeling rule by
assigning exactly one complexity label for the PR.
- Line 284: The modal labels in ResultsPage are still hardcoded English, which
is inconsistent with the rest of the localized UI. Update the strings used in
the comparison modal and cards in ResultsPage—especially the `comparisonLoading`
fallback, the `SPECIMEN A`/`SPECIMEN B` headers, and the `GILLS`/`EYES`/`SCALES`
labels—to use `t(...)` like the existing `results.specimenA` usage. Add or wire
up the corresponding bn/hi/en translation keys for these labels so the same
symbols render correctly across locales.
- Around line 293-301: The comparison modal in ResultsPage is missing
accessibility and keyboard dismissal support. Update the modal container around
the comparison view to use dialog semantics with aria-modal and an accessible
label, add an Escape-to-close handler (for example via a useEffect tied to
comparisonData), allow backdrop clicks to close the modal, and give the
icon-only close button an aria-label so screen readers can identify it. Use the
existing setComparisonData(null) close path and the modal overlay/button markup
in ResultsPage to wire these fixes in.
- Line 19: The comparisonData state in ResultsPage should use the real scan
response type instead of any, since the modal accesses fields from the scan
shape and the current type is triggering the no-explicit-any lint error. Update
the useState declaration for comparisonData to reference the ScanResponse shape
returned by api.getScan, and keep the nested scan1/scan2 objects typed
consistently so accesses like species.common_name and
biomarkers.gill_saturation.score remain type-checked.
In `@src/pages/ScannerPage.tsx`:
- Line 149: The ScannerPage sync toast setup has an unused binding: the return
value from toast.loading in the offline sync flow is assigned to syncToastId but
never referenced. Remove that assignment and continue using the existing literal
id path in the ScannerPage background sync logic (including the toast
update/dismiss calls) so the no-unused-vars lint issue is resolved without
changing behavior.
- Line 492: The SYNC NOW control in ScannerPage’s button markup should
explicitly avoid implicit form submission. Update the `<button>` used for this
action to include `type="button"` so it stays a normal action button even if it
is rendered inside a form; locate the button in the ScannerPage component where
the sync action is defined.
- Line 337: The offline ID suffix in ScannerPage uses the deprecated
String.prototype.substr call. Update the offlineId generation logic to avoid
substr by using slice(2, 11) on the Math.random().toString(36) result, or switch
to crypto.randomUUID() if that fits the surrounding code better. Keep the change
localized to the offlineId creation expression so the ScannerPage flow remains
unchanged.
- Line 371: The catch block in ScannerPage’s fallback branch declares an unused
error variable, which triggers the lint rule; update the `catch (err)` in the
relevant error-handling path to use an optional catch binding since the error is
not referenced. Keep the surrounding fallback logic in `ScannerPage` the same,
and adjust only the catch syntax so the branch still handles failures without
introducing an unused `err` binding.
- Around line 477-501: ScannerPage renders GlassCard in the pending-scans banner
but does not import it, so the component will fail at render time when
pendingCount is greater than zero. Add the missing GlassCard import in
ScannerPage.tsx alongside the other top-level imports, and verify the pending
sync banner still renders correctly when syncingScans and checkAndSyncScans are
used.
---
Outside diff comments:
In `@src/pages/ScannerPage.tsx`:
- Line 397: The runScan callback in ScannerPage.tsx is missing t in its
dependency array, so the translator captured by useCallback can become stale
after a language change. Update the dependency list for runScan to include t,
and verify any related hooks in ScannerPage still satisfy exhaustive-deps after
the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3333dded-a91c-44c0-955a-b35ebaf7cad5
📒 Files selected for processing (14)
backend/main.pybackend/vendors.pysrc/components/AnalyticsTrends.tsxsrc/fusionInference.jssrc/i18n/locales/bn.jsonsrc/i18n/locales/en.jsonsrc/i18n/locales/hi.jsonsrc/lib/api.tssrc/lib/offlineDb.tssrc/lib/types.tssrc/pages/AnalysisDashboard.tsxsrc/pages/Leaderboard.tsxsrc/pages/ResultsPage.tsxsrc/pages/ScannerPage.tsx
| gill_quality = round(random.uniform(0.85, 0.98), 2) | ||
| eye_quality = round(random.uniform(0.80, 0.96), 2) | ||
| body_quality = round(random.uniform(0.90, 0.99), 2) | ||
|
|
||
| raw_w_body = 0.50 * body_quality | ||
| raw_w_eye = 0.25 * eye_quality | ||
| raw_w_gill = 0.25 * gill_quality | ||
| sum_w = raw_w_body + raw_w_eye + raw_w_gill | ||
| weight_body = round(raw_w_body / sum_w, 2) | ||
| weight_eye = round(raw_w_eye / sum_w, 2) | ||
| weight_gill = round(raw_w_gill / sum_w, 2) | ||
|
|
||
| mean_score = (gill_score + eye_score + body_score) / 3 | ||
| variance = ((gill_score-mean_score)**2 + (eye_score-mean_score)**2 + (body_score-mean_score)**2) / 3 | ||
| std_dev = round(math.sqrt(variance), 1) | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map backend/main.py structure first.
ast-grep outline backend/main.py --view expanded || true
# Show the relevant line ranges with context.
sed -n '230,310p' backend/main.py
printf '\n---\n'
sed -n '340,420p' backend/main.py
printf '\n---\n'
sed -n '600,840p' backend/main.pyRepository: jpdevhub/FreshScanAi
Length of output: 19033
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find all uses of the ensemble payload fields to see whether they affect logic or are display-only.
rg -n '"ensemble"|qualities|weights|margin_of_error|gill_quality|eye_quality|body_quality' backend frontend . || trueRepository: jpdevhub/FreshScanAi
Length of output: 6671
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect documentation around the intended ensemble weight semantics.
sed -n '280,320p' DOCUMENTATION.md
printf '\n---\n'
sed -n '290,330p' src/pages/AnalysisDashboard.tsxRepository: jpdevhub/FreshScanAi
Length of output: 3679
Use real quality signals or fixed weights here. backend/main.py:257-307 and the matching _row_to_payload block build ensemble.weights from random.uniform(...), so the dashboard shows per-stream weights that have no relationship to the scan or the documented 50/25/25 weighting.
🧰 Tools
🪛 ast-grep (0.44.0)
[info] 257-257: use secrets package over random package
Context: random.uniform(0.80, 0.96)
Note: [CWE-330] Use of Insufficiently Random Values.
(avoid-random-python)
[info] 258-258: use secrets package over random package
Context: random.uniform(0.90, 0.99)
Note: [CWE-330] Use of Insufficiently Random Values.
(avoid-random-python)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` around lines 257 - 272, The ensemble weight calculation in
the main scan payload is using random.uniform values, so replace the randomized
quality signals and derived weights with real scan-driven metrics or a fixed
50/25/25 weighting in the payload-building logic. Update the weight computation
in the block that sets gill_quality, eye_quality, body_quality, weight_body,
weight_eye, and weight_gill, and make the matching _row_to_payload path use the
same deterministic source so ensemble.weights reflects the actual documented
weighting instead of per-request randomness.
| "weight_estimate_kg": 1.2, | ||
| "catch_age_hours": 6, | ||
| }, | ||
| "uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Falsy-default masks a low-confidence row.
(row.get("confidence_score") or 1.0) < 0.70 treats a stored confidence_score of 0 (or 0.0) as 1.0, flipping a maximally-uncertain scan to "certain". Use an explicit None check so only missing values fall back.
🐛 Proposed fix
- "uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70,
+ "uncertain_flag": (row.get("confidence_score") if row.get("confidence_score") is not None else 1.0) < 0.70,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "uncertain_flag": (row.get("confidence_score") or 1.0) < 0.70, | |
| "uncertain_flag": (row.get("confidence_score") if row.get("confidence_score") is not None else 1.0) < 0.70, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` at line 393, The uncertain_flag expression in the row
processing logic is using a falsy fallback, so a real confidence_score of 0 or
0.0 gets replaced with 1.0 and marked certain. Update the confidence handling in
the code path that builds the row dict in main.py to use an explicit None check
instead of “or 1.0”, so only missing confidence_score values default while valid
zero values are preserved.
| if source == "edge_onnx" and fused_score is not None: | ||
| freshness = int(fused_score * 100) | ||
| conf = confidence_score or 0.85 | ||
| edge_fusion = { | ||
| "final_score_percent": freshness, | ||
| "final_grade": _to_db_grade(freshness_label or "C"), | ||
| "confidence_score": conf, | ||
| "uncertain_prediction_flag": conf < 0.70, | ||
| "regional_breakdown": { | ||
| "gill_freshness_score": fused_score, | ||
| "eye_freshness_score": fused_score, | ||
| "body_freshness_score": fused_score, | ||
| }, | ||
| } | ||
| photo_url = await _upload_image(image_bytes, str(current_user.id), scan_id) | ||
| payload = _build_scan_payload(edge_fusion, scan_id, display_id, photo_url) | ||
| if species_detected: | ||
| payload["species"]["common_name"] = species_detected | ||
|
|
||
| try: | ||
| _db().table("scans").insert( | ||
| { | ||
| "id": scan_id, | ||
| "user_id": str(current_user.id), | ||
| "final_grade": _to_db_grade(payload["grade"]), | ||
| "confidence_score": conf, | ||
| "image_type": "BODY", | ||
| "freshness_index": payload["freshness_index"], | ||
| "scan_display_id": display_id, | ||
| "species_detected": species_detected or "Rohu Carp", | ||
| "biomarker_json": payload["biomarkers"], | ||
| "storage_hours": payload["recommendations"]["consume_within_hours"], | ||
| "alert_flags": payload["recommendations"]["alert_flags"], | ||
| "photo_urls": [photo_url] if photo_url else [], | ||
| } | ||
| ).execute() | ||
| except Exception as exc: | ||
| print(f"DB write failed (edge_onnx): {exc}") | ||
|
|
||
| return {"success": True, "scan": payload} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -nP -C3 'def _to_db_grade' backend/main.pyRepository: jpdevhub/FreshScanAi
Length of output: 388
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant backend helpers and edge_onnx path.
sed -n '224,310p' backend/main.py
printf '\n----\n'
sed -n '620,710p' backend/main.py
printf '\n----\n'
rg -n -C3 '_build_scan_payload|freshness_label|fused_score|edge_onnx' backend/main.py src/lib/api.ts
printf '\n----\n'
sed -n '1,220p' src/lib/api.tsRepository: jpdevhub/FreshScanAi
Length of output: 18343
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# If the first read is too large, inspect only the submitScan function and any grade mappings.
rg -n -C4 'submitScan|freshness_label|grade' src/lib/api.ts
printf '\n----\n'
rg -n -C4 'def _build_scan_payload|payload\["grade"\]|final_grade|freshness_index' backend/main.pyRepository: jpdevhub/FreshScanAi
Length of output: 9494
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the client payload shape and the payload builder semantics.
rg -n -C4 'submitScan|freshness_label|freshnessLabel|grade|final_grade' src/lib/api.ts src -g '!**/node_modules/**'
printf '\n----\n'
python3 - <<'PY'
from pathlib import Path
p = Path('backend/main.py')
text = p.read_text()
for needle in ['def _build_scan_payload', 'payload["grade"]', 'final_grade', 'freshness_index', 'def _to_db_grade']:
idx = text.find(needle)
print(f'NEEDLE: {needle} -> {idx}')
PYRepository: jpdevhub/FreshScanAi
Length of output: 31070
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the backend source of freshness_label and the client submitScan payload.
rg -n -C4 'freshness_label|freshnessLabel|submitScan|final_grade|_build_scan_payload|_to_db_grade' backend/main.py src/lib/api.ts
printf '\n----\n'
sed -n '1,260p' src/lib/api.ts
printf '\n----\n'
sed -n '600,710p' backend/main.pyRepository: jpdevhub/FreshScanAi
Length of output: 24457
Map the edge ONNX label to the stored grade enum
freshness_label is "Fresh"/"Moderate"/"Spoiled", but this path writes it into final_grade. The rest of the app treats final_grade as the grade used by the demo/real paths, so synced edge scans will show the wrong grade in history/dashboard. _to_db_grade is a no-op here, so it doesn't translate the label; convert the edge label to the same stored grade before inserting.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/main.py` around lines 663 - 702, The edge ONNX scan path is storing
the human-readable freshness label directly into the scan grade field, so
history/dashboard records the wrong grade. In the edge_onnx branch inside the
scan insert flow, map freshness_label to the same stored grade enum used by the
demo/real paths before calling _db().table("scans").insert, and ensure the value
passed through _to_db_grade matches the app’s existing grade format rather than
the raw "Fresh"/"Moderate"/"Spoiled" label.
| # In-memory reviews database fallback | ||
| _REVIEWS_DB = {} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
In-memory _REVIEWS_DB will not persist or scale.
State stored in a module-level dict is lost on every restart/redeploy and is not shared across multiple Uvicorn/Gunicorn workers, so reviews will appear and disappear depending on which process serves the request. If this is a temporary stub, please add a TODO and issue; otherwise persist to the same store used by vendors/scans.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/vendors.py` around lines 8 - 9, The module-level _REVIEWS_DB fallback
in the vendors module is only in-memory and will not survive restarts or work
across multiple workers. Replace this temporary storage with the same persistent
backend used by vendors and scans, or if it must remain as a stub, add an
explicit TODO with an issue reference so it is clearly temporary. Update the
code around _REVIEWS_DB and any review accessors to use the shared persistent
store instead of a dict.
| reviews = _REVIEWS_DB.get(vendor_id, [ | ||
| { | ||
| "id": "rev-1", | ||
| "author": "Ankit R.", | ||
| "rating": 5, | ||
| "comment": "Consistently fresh rohu fish. Highly recommended!", | ||
| "timestamp": (datetime.now(timezone.utc) - timedelta(days=2)).isoformat() | ||
| }, | ||
| { | ||
| "id": "rev-2", | ||
| "author": "Deepika S.", | ||
| "rating": 4, | ||
| "comment": "Good quality scales, operculum is bright red. Fair pricing.", | ||
| "timestamp": (datetime.now(timezone.utc) - timedelta(days=5)).isoformat() | ||
| } | ||
| ]) | ||
| return {"success": True, "reviews": reviews} | ||
|
|
||
| @router.post("/{vendor_id}/reviews") | ||
| async def add_vendor_review( | ||
| vendor_id: str, | ||
| review_data: dict, | ||
| request: Request | ||
| ): | ||
| author = "Anonymous Consumer" | ||
| try: | ||
| auth_header = request.headers.get("Authorization") | ||
| if auth_header: | ||
| # We can call get_current_user dynamically | ||
| user = await get_current_user(request) | ||
| if user: | ||
| author = user.user_metadata.get("full_name") or user.email | ||
| except Exception: | ||
| pass | ||
|
|
||
| rating = review_data.get("rating", 5) | ||
| comment = review_data.get("comment", "") | ||
|
|
||
| new_review = { | ||
| "id": f"rev-{datetime.now(timezone.utc).timestamp()}", | ||
| "author": author, | ||
| "rating": int(rating), | ||
| "comment": comment, | ||
| "timestamp": datetime.now(timezone.utc).isoformat() | ||
| } | ||
|
|
||
| if vendor_id not in _REVIEWS_DB: | ||
| _REVIEWS_DB[vendor_id] = [ | ||
| { | ||
| "id": "rev-1", | ||
| "author": "Ankit R.", | ||
| "rating": 5, | ||
| "comment": "Consistently fresh rohu fish. Highly recommended!", | ||
| "timestamp": (datetime.now(timezone.utc) - timedelta(days=2)).isoformat() | ||
| }, | ||
| { | ||
| "id": "rev-2", | ||
| "author": "Deepika S.", | ||
| "rating": 4, | ||
| "comment": "Good quality scales, operculum is bright red. Fair pricing.", | ||
| "timestamp": (datetime.now(timezone.utc) - timedelta(days=5)).isoformat() | ||
| } | ||
| ] |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Duplicate default-review block.
The identical seed list is hardcoded in both the GET fallback (Lines 104–119) and the POST seeding (Lines 151–166). Extract a _default_reviews() factory so the two copies cannot diverge.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/vendors.py` around lines 104 - 166, The default review seed is
duplicated in both the GET fallback and the POST initialization inside the
vendors review handlers, which risks them drifting apart. Extract the repeated
seed list into a shared helper such as _default_reviews() in the same module and
use it from both the reviews lookup and add_vendor_review paths so the seed data
is defined in one place only.
| if (pending.length === 0) return; | ||
|
|
||
| setSyncingScans(true); | ||
| const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Remove the unused syncToastId binding.
toast.loading(...) returns an id that is never read — you dismiss/update using the literal 'offline-sync' id at Lines 175/177/182. This fails the @typescript-eslint/no-unused-vars lint gate.
♻️ Drop the unused assignment
- const syncToastId = toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });
+ toast.loading(t('scanner.syncingScans', 'Syncing offline scans in background...'), { id: 'offline-sync' });Also applies to: 174-178
🧰 Tools
🪛 ESLint
[error] 149-149: 'syncToastId' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` at line 149, The ScannerPage sync toast setup has
an unused binding: the return value from toast.loading in the offline sync flow
is assigned to syncToastId but never referenced. Remove that assignment and
continue using the existing literal id path in the ScannerPage background sync
logic (including the toast update/dismiss calls) so the no-unused-vars lint
issue is resolved without changing behavior.
Source: Linters/SAST tools
| canvas.toBlob( | ||
| async (saveBlob) => { | ||
| if (!saveBlob) return; | ||
| const offlineId = `offline-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
String.prototype.substr is deprecated — use slice.
substr is a legacy Annex B feature; prefer slice(2, 11) (or crypto.randomUUID()) for the offline id suffix.
♻️ Replace substr
- const offlineId = `offline-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ const offlineId = `offline-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const offlineId = `offline-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| const offlineId = `offline-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` at line 337, The offline ID suffix in ScannerPage
uses the deprecated String.prototype.substr call. Update the offlineId
generation logic to avoid substr by using slice(2, 11) on the
Math.random().toString(36) result, or switch to crypto.randomUUID() if that fits
the surrounding code better. Keep the change localized to the offlineId creation
expression so the ScannerPage flow remains unchanged.
| } | ||
| } catch { | ||
| /* offline or backend down — result still shown locally */ | ||
| } catch (err) { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Unused err binding in catch — lint failure.
err at Line 371 is never referenced in the fallback branch, tripping @typescript-eslint/no-unused-vars. Use an optional catch binding (catch {) since the caught error isn't used.
♻️ Use optional catch binding
- } catch (err) {
+ } catch {
await offlineDb.addScan(offlineScanRecord);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| } catch (err) { | |
| } catch { |
🧰 Tools
🪛 ESLint
[error] 371-371: 'err' is defined but never used.
(@typescript-eslint/no-unused-vars)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` at line 371, The catch block in ScannerPage’s
fallback branch declares an unused error variable, which triggers the lint rule;
update the `catch (err)` in the relevant error-handling path to use an optional
catch binding since the error is not referenced. Keep the surrounding fallback
logic in `ScannerPage` the same, and adjust only the catch syntax so the branch
still handles failures without introducing an unused `err` binding.
Source: Linters/SAST tools
| {pendingCount > 0 && ( | ||
| <div className="absolute top-4 inset-x-4 z-40"> | ||
| <GlassCard className="p-3 border-l-4 border-secondary! flex items-center justify-between" variant="tonal"> | ||
| <div className="flex items-center gap-2"> | ||
| <span className="relative flex h-2 w-2"> | ||
| <span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} /> | ||
| <span className={`relative inline-flex rounded-full h-2 w-2 ${syncingScans ? 'bg-secondary' : 'bg-neon'}`} /> | ||
| </span> | ||
| <span className="font-mono text-[0.625rem] tracking-widest text-on-surface uppercase"> | ||
| {syncingScans | ||
| ? t('scanner.syncingScans', 'Syncing offline scans in background...') | ||
| : `${pendingCount} ${t('scanner.pendingSyncScans', 'OFFLINE SCANS PENDING SYNC')}`} | ||
| </span> | ||
| </div> | ||
| {!syncingScans && navigator.onLine && ( | ||
| <button | ||
| onClick={checkAndSyncScans} | ||
| className="font-mono text-[0.55rem] tracking-widest bg-secondary text-on-primary px-3 py-1 font-bold border-none hover:bg-neon transition-colors cursor-pointer" | ||
| > | ||
| {t('scanner.syncNowButton', 'SYNC NOW')} | ||
| </button> | ||
| )} | ||
| </GlassCard> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -i glasscard -e tsx -e ts | xargs -I{} sh -c 'echo "== {} =="; rg -nP "export (default |const |function )?GlassCard" {}'Repository: jpdevhub/FreshScanAi
Length of output: 197
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '== ScannerPage imports ==\n'
sed -n '1,40p' src/pages/ScannerPage.tsx
printf '\n== GlassCard component ==\n'
sed -n '1,200p' src/components/GlassCard.tsxRepository: jpdevhub/FreshScanAi
Length of output: 2306
Import GlassCard in src/pages/ScannerPage.tsx. It’s rendered in the pending-scans banner but never imported, so the first time pendingCount > 0 the page throws a ReferenceError during render.
🧰 Tools
🪛 React Doctor (0.5.8)
[error] 479-479: GlassCard crashes at runtime because it isn't defined here.
Import the component or fix the typo so React can resolve the JSX identifier at runtime.
(jsx-no-undef)
[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.
Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".
(button-has-type)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` around lines 477 - 501, ScannerPage renders
GlassCard in the pending-scans banner but does not import it, so the component
will fail at render time when pendingCount is greater than zero. Add the missing
GlassCard import in ScannerPage.tsx alongside the other top-level imports, and
verify the pending sync banner still renders correctly when syncingScans and
checkAndSyncScans are used.
Source: Linters/SAST tools
| </span> | ||
| </div> | ||
| {!syncingScans && navigator.onLine && ( | ||
| <button |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
Set an explicit type="button" on the SYNC NOW button.
Per the React Doctor hint, a <button> without a type defaults to submit; add type="button" to guard against accidental form submission if this is ever nested in a form.
🧰 Tools
🪛 React Doctor (0.5.8)
[warning] 492-492: Your users can submit the form by accident because a <button> with no type defaults to submit.
Set an explicit button type so plain buttons do not submit forms by accident: type="button", "submit", or "reset".
(button-has-type)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/pages/ScannerPage.tsx` at line 492, The SYNC NOW control in ScannerPage’s
button markup should explicitly avoid implicit form submission. Update the
`<button>` used for this action to include `type="button"` so it stays a normal
action button even if it is rendered inside a form; locate the button in the
ScannerPage component where the sync action is defined.
Source: Linters/SAST tools
🔗 Upstream Issue Connection
Closes #169
This Pull Request is officially linked to and resolves Issue #169 (Feature 10: Multi-Model AI Ensemble Engine) in the upstream repository.
Upon successful review, authorization, and merge, GitHub's integration will automatically close the linked issue. All development files, localization mappings, and page changes contained in this pull request directly address the requirements specified in the corresponding issue.
What changes are made?
backend/main.py): Built an input-quality adaptive ensembling model. Gills, Eyes, and Body weights are recalculated dynamically based on their respective quality scores.src/pages/AnalysisDashboard.tsx): Added a layout section in the main scorecard rendering body, eye, and gill contribution percentages.Technical Depth and Verification
Fixed weighting architectures fail when input crops are compromised. Our dynamic ensemble weights dynamically scale stream contributions according to quality, improving prediction accuracy. If one model fails, the engine re-normalizes the remaining active weights seamlessly.
Verified on the scan dashboard. The scorecard correctly displays the dynamic contribution percentages of body, eye, and gill models.
Summary by CodeRabbit
New Features
Bug Fixes